/*
 * FlamingIRC IRC library
 * Copyright (C) 2008 Brian Ortiz & Max Schmeling <http://code.google.com/p/ortzirc>
 * 
 * Based on code copyright (C) 2002 Aaron Hunter <thresher@sharkbite.org>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 * 
 * See the gpl.txt file located in the top-level-directory of
 * the archive of this library for complete text of license.
*/

namespace FlamingIRC
{
    using System;
    using System.Text;

    /// <summary>
    /// This class is used to send all the IRC commands except for CTCP and DCC
    /// messages. Instances of this class are retrieved as properties of the Connection
    /// object. All methods in this class are thread safe.
    /// </summary>
    /// <remarks>
    /// <para>Due to the asynchronous nature of IRC, none of these commands 
    /// have a return value. To get that value (or possibly an error) the client must
    /// handle the corresponding event. For example, to check if a user is online
    /// the client would send <see cref="Sender.Ison"/> then check the value of the 
    /// <see cref="Listener.OnIson"/> event to receive the answer.</para>
    /// <para>When a command can return an error, the possible error replies
    /// are listed. An error message will be sent via the <see cref="Listener.OnError"/> event
    /// with one of the listed error codes as a parameter. When checking for these 
    /// errors use the constants from <see cref="ReplyCode"/>.
    /// </para> 
    /// <para>The maximum length of any command string sent to the 
    /// server is 512 characters.</para>
    /// </remarks>
    /// <example><code>
    /// //Create a Connection object which will automatically create its own Sender
    /// Connection connection = new Connection( args, false, false );	
    /// //Send commands using the Connection object and its Sender instance.
    /// //No need to keep a separate reference to the Sender object
    /// connection.Sender.PublicMessage("#thresher", "hello");
    /// </code></example>
    public class Sender : CommandBuilder
    {
        /// <summary>
        /// Create a new Sender for a specific connection.
        /// </summary>
        internal Sender(Connection connection) : base(connection) { }

        private bool IsEmpty(string aString)
        {
            return aString == null || aString.Trim().Length == 0;
        }

        /// <summary>
        /// Truncate parameters which cause a command line
        /// to be too long.
        /// </summary>
        /// <param name="parameter">The command parameter</param>
        /// <param name="commandLength">The length of the command plus whitespace</param>
        /// <returns></returns>
        private string Truncate(string parameter, int commandLength)
        {
            int max = MAX_COMMAND_SIZE - commandLength;
            if (parameter.Length > max)
            {
                return parameter.Substring(0, max);
            }

            return parameter;
        }

        private bool TooLong(StringBuilder buffer)
        {
            //2 for CR LF
            return (buffer.Length + 2) > MAX_COMMAND_SIZE;
        }

        /// <summary>
        /// The USER command is only used at the beginning of Connection to specify
        /// the username, hostname and realname of a new user.
        /// </summary>
        /// <param name="args">The user Connection data</param>
        internal void User(ConnectionArgs args)
        {
            lock (this)
            {
                Buffer.Append("USER");
                Buffer.Append(SPACE);
                Buffer.Append(args.UserName);
                Buffer.Append(SPACE);
                Buffer.Append(args.ModeMask);
                Buffer.Append(SPACE);
                Buffer.Append('*');
                Buffer.Append(SPACE);
                Buffer.Append(':');
                Buffer.Append(args.RealName);
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// A client session is terminated with a quit message.
        /// </summary>
        /// <remarks> 
        /// <para>The server
        /// acknowledges this by sending an ERROR message to the client. 
        /// </para>
        /// <para>Before closing the Connection with the IRC server this method
        /// will call <c>Listener.beforeDisconnect()</c> and after
        /// the Connection is closed it will call <c> Listener.OnDisconnect()</c>
        /// </para>
        /// </remarks>
        /// <param name="reason">Reason for quitting.</param>
        internal void Quit(string reason)
        {
            lock (this)
            {
                Buffer.Append("QUIT");
                if (IsEmpty(reason))
                {
                    ClearBuffer();
                    throw new ArgumentException("Quit reason cannot be null or empty.");
                }
                Buffer.Append(SPACE_COLON);
                if (reason.Length > 502)
                {
                    reason = reason.Substring(0, 504);
                }
                Buffer.Append(reason);
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// A PONG message is a reply to server PING message. Only called by
        /// the Connection object to keep the Connection alive.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// <list type="bullet">
        /// 			<item><description>ERR_NOORIGIN</description></item>
        /// 			<item><description>ERR_NOSUCHSERVER</description></item>
        /// </list>
        /// </remarks>
        /// <param name="message">The text sent by the IRC server in the PING message.</param>
        internal void Pong(string message)
        {
            //Not synchronized because it will only be called during on OnPing event by
            //the dispatch thread
            Buffer.Append("PONG");
            Buffer.Append(SPACE);
            Buffer.Append(message);
            Connection.SendAutomaticReply(Buffer);
        }

        /// <summary>
        /// The PASS command is used to set a 'Connection password'. 
        /// </summary>
        /// <remarks>
        /// The optional password can and MUST be set before any attempt to register
        /// the Connection is made. Currently this requires that user send a
        /// PASS command before sending the NICK/USER combination.
        /// </remarks>
        internal void Pass(string password)
        {
            lock (this)
            {
                Buffer.Append("PASS");
                Buffer.Append(SPACE);
                Buffer.Append(password);
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// User registration consists of 3 commands:
        /// 1. PASS
        /// 2. NICK
        /// 3. USER
        /// Pass will rarely fail but the proposed Nick might already be taken in
        /// which case the client will have to register by manually calling Nick
        /// and User.
        /// </summary>
        internal void RegisterConnection(ConnectionArgs args)
        {
            Pass(args.ServerPassword);
            Nick(args.Nick);
            User(args);
        }

        /// <summary>
        /// Join the specified channel. 
        /// </summary>
        /// <remarks>
        /// <para>Once a user has joined a channel, he receives information about
        /// all commands his server receives affecting the channel. This
        /// includes JOIN, MODE, KICK, PART, QUIT and of course PRIVMSG/NOTICE.
        /// This allows channel members to keep track of the other channel
        /// members, as well as channel modes.</para>
        /// <para>If a JOIN is successful, the user receives a JOIN message as
        /// confirmation and is then sent the channel's topic ( <see cref="Listener.OnRecieveTopic"/> and
        /// the list of users who are on the channel ( <see cref="Listener.OnNames"/> ), which
        /// MUST include the user joining.</para>
        /// 
        /// Possible Errors
        /// <list type="bullet">
        /// 	<item><description>ERR_NEEDMOREPARAMS</description></item>
        /// 	<item><description>ERR_BANNEDFROMCHAN</description></item>
        /// 	<item><description>ERR_INVITEONLYCHAN</description></item>
        /// 	<item><description>ERR_BADCHANNELKEY</description></item>
        /// 	<item><description>ERR_CHANNELISFULL</description></item>
        /// 	<item><description>ERR_BADCHANMASK</description></item>
        /// 	<item><description>ERR_NOSUCHCHANNEL</description></item>
        /// 	<item><description>ERR_TOOMANYCHANNELS</description></item>
        /// 	<item><description>ERR_TOOMANYTARGETS</description></item>
        /// 	<item><description>ERR_UNAVAILRESOURCE</description></item>
        /// </list>
        /// </remarks>
        /// <param name="channel">The channel to join. Channel names must begin with '&amp;', '#', '+' or '!'.</param>
        /// <example><code>
        /// //Most channels you will see begin with the '#'. The others are reserved
        /// //for special channels and may not even be available on a particular server.
        /// connection.Sender.Join("#thresher");
        /// </code></example>
        /// <exception cref="ArgumentException">If the channel name is not valid.</exception>
        /// <seealso cref="Listener.OnJoin"/>
        public void Join(string channel)
        {
            Join(channel, String.Empty);
        }

        /// <summary>
        /// Join a passworded channel.
        /// </summary>
        /// <param name="channel">Channel to join</param>
        /// <param name="password">The channel's password. If empty, joins without a password.</param>
        /// <exception cref="ArgumentException">If the channel name is not valid or the password is null.</exception> 
        /// <seealso cref="Listener.OnJoin"/>
        public void Join(string channel, string password)
        {
            lock (this)
            {
                if (password == null)
                {
                    ClearBuffer();
                    throw new ArgumentException("Password cannot be null.");
                }

                if (Rfc2812Util.IsValidChannelName(channel))
                {
                    Buffer.Append("JOIN");
                    Buffer.Append(SPACE);
                    Buffer.Append(channel);

                    if (password != String.Empty)
                    {
                        Buffer.Append(SPACE);
                        //8 is the JOIN + 2 spaces + CR + LF
                        password = Truncate(password, 8);
                        Buffer.Append(password);
                    }

                    Connection.SendCommand(Buffer);
                }
                else
                {
                    ClearBuffer();
                    throw new ArgumentException(channel + " is not a valid channel name.");
                }
            }
        }
        /// <summary>
        /// Change the user's nickname.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 	<list type="bullet">
        /// 		<item><description>ERR_NONICKNAMEGIVEN</description></item>
        /// 		<item><description>ERR_ERRONEUSNICKNAME</description></item>
        /// 		<item><description>ERR_NICKNAMEINUSE</description></item>
        /// 		<item><description>ERR_NICKCOLLISION</description></item>
        /// 		<item><description>ERR_UNAVAILRESOURCE</description></item>
        /// 		<item><description>ERR_RESTRICTED</description></item>
        /// 	</list>
        /// </remarks>
        /// <param name="newNick"> The new nickname</param>
        /// <example><code>
        /// //Make sure and verify that the nick is valid and of the right length
        /// string nick = GetUserInput();
        /// if( Rfc2812Util.IsValidNick( connection, nick) ) { 
        /// connection.Sender.Nick( nick );
        /// }
        /// </code></example>
        /// <exception cref="ArgumentException">If the nickname is not valid.</exception> 
        /// <seealso cref="Listener.OnNick"/>
        public void Nick(string newNick)
        {
            lock (this)
            {
                if (Rfc2812Util.IsValidNick(newNick))
                {
                    Buffer.Append("NICK");
                    Buffer.Append(SPACE);
                    Buffer.Append(newNick);
                    Connection.SendCommand(Buffer);
                }
                else
                {
                    ClearBuffer();
                    throw new ArgumentException(newNick + " is not a valid nickname.");
                }
            }
        }
        /// <summary> 
        /// Request a list of all nicknames on a given channel.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// <list type="bullet">
        /// 		<item><description>ERR_TOOMANYMATCHES</description></item>
        /// </list>
        /// </remarks>
        /// <param name="channels">One or more channel names.</param>
        /// <example><code>
        /// //Make the request for a single channel
        /// connection.Sender.Names( "#test" );
        /// //Make the request for several channels at once
        /// connection.Sender.Names( "#test","#alpha","#bravo" );
        /// </code></example>
        /// <exception cref="ArgumentException">If any of the channels are not valid.</exception> 
        /// <seealso cref="Listener.OnNames"/>
        public void Names(params string[] channels)
        {
            lock (this)
            {
                if (Rfc2812Util.IsValidChannelList(channels))
                {
                    Buffer.Append("NAMES");
                    Buffer.Append(SPACE);
                    Buffer.Append(String.Join(",", channels));
                    if (TooLong(Buffer))
                    {
                        ClearBuffer();
                        throw new ArgumentException("Channels are too long.");
                    }
                    Connection.SendCommand(Buffer);
                }
                else
                {
                    ClearBuffer();
                    throw new ArgumentException("One of the channel names is not valid.");
                }
            }
        }
        /// <summary>
        /// Request a list of all visible channels along with their users. If the server allows this
        /// kind of request then expect a rather large reply. 
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 	<list type="bullet">
        /// 		<item><description>ERR_TOOMANYMATCHES</description></item>
        /// 	</list>
        /// </remarks> 
        /// <seealso cref="Listener.OnNames"/> 
        public void AllNames()
        {
            lock (this)
            {
                Buffer.Append("NAMES");
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>Request basic information about a channel, i.e. number
        /// of visible users and topic.</summary>
        /// <remarks>
        /// Possible Errors
        /// 	<list type="bullet">
        /// 		<item><description>ERR_TOOMANYMATCHES</description></item>
        /// </list>
        /// </remarks> 
        /// <param name="channels">One or more channel names.</param>
        /// <example><code>
        /// //Make the request for a single channel
        /// connection.Sender.List( "#test" );
        /// //Make the request for several channels at once
        /// connection.Sender.List( "#test","#alpha",#"bravo" );
        /// </code></example>
        /// <exception cref="ArgumentException">If any of the channels are not valid.</exception> 
        /// <seealso cref="Listener.OnList"/> 
        public void List(params string[] channels)
        {
            lock (this)
            {
                if (Rfc2812Util.IsValidChannelList(channels))
                {
                    Buffer.Append("LIST");
                    Buffer.Append(SPACE);
                    Buffer.Append(String.Join(",", channels));
                    if (TooLong(Buffer))
                    {
                        ClearBuffer();
                        throw new ArgumentException("Channels are too long.");
                    }
                    Connection.SendCommand(Buffer);
                }
                else
                {
                    ClearBuffer();
                    throw new ArgumentException("One of the channel names is not valid.");
                }
            }
        }
        /// <summary>
        /// Request basic information for all the channels on the current
        /// network.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 	<list type="bullet">
        /// 		<item><description>ERR_TOOMANYMATCHES</description></item>
        /// </list>
        /// </remarks> 
        /// <seealso cref="Listener.OnList"/>
        public void AllList()
        {
            lock (this)
            {
                Buffer.Append("LIST");
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>Change the topic of the given channel.</summary>
        /// <remarks>
        /// Possible Errors
        /// <list type="bullet">
        /// 		<item><description>ERR_NEEDMOREPARAMS</description></item>
        /// 		<item><description>ERR_NOTONCHANNEL</description></item>
        /// 		<item><description>ERR_CHANOPRIVSNEEDED</description></item>
        /// 		<item><description>ERR_NOCHANMODES</description></item>
        /// </list>
        /// </remarks>
        /// <param name="channel">The target channel.</param>
        /// <param name="newTopic">The new topic.</param>
        /// <example><code>
        /// connection.Sender.ChangeTopic( "#thresher","Beta 27 Released" );
        /// </code></example>	
        /// <exception cref="ArgumentException">If the channel name is not valid or the topic is null.</exception> 
        /// <seealso cref="Listener.OnTopicChanged"/> 
        public void ChangeTopic(string channel, string newTopic)
        {
            lock (this)
            {
                if (IsEmpty(newTopic))
                {
                    ClearBuffer();
                    throw new ArgumentException("Topic cannot be empty or null.");
                }
                if (Rfc2812Util.IsValidChannelName(channel))
                {
                    Buffer.Append("TOPIC");
                    Buffer.Append(SPACE);
                    Buffer.Append(channel);
                    Buffer.Append(SPACE_COLON);
                    // 9 is TOPIC + 2 x Spaces + : + CR = LF
                    newTopic = Truncate(newTopic, 9 + channel.Length);
                    Buffer.Append(newTopic);
                    Connection.SendCommand(Buffer);
                }
                else
                {
                    ClearBuffer();
                    throw new ArgumentException(channel + " is not a valid channel name.");
                }
            }
        }
        /// <summary>Clear the channel's topic.</summary>
        /// <remarks>
        /// Possible Errors
        /// <list type="bullet">
        /// 		<item><description>ERR_NEEDMOREPARAMS</description></item>
        /// 		<item><description>ERR_NOTONCHANNEL</description></item>
        /// 		<item><description>ERR_CHANOPRIVSNEEDED</description></item>
        /// 		<item><description>ERR_NOCHANMODES</description></item>
        /// </list>
        /// </remarks>
        /// <param name="channel">The target channel.</param>
        /// <exception cref="ArgumentException">If the channel name is not valid.</exception> 
        /// <seealso cref="Listener.OnTopicChanged"/> 
        public void ClearTopic(string channel)
        {
            lock (this)
            {
                if (Rfc2812Util.IsValidChannelName(channel))
                {
                    Buffer.Append("TOPIC");
                    Buffer.Append(SPACE);
                    Buffer.Append(channel);
                    Buffer.Append(SPACE_COLON);
                    Buffer.Append(SPACE);
                    Connection.SendCommand(Buffer);
                }
                else
                {
                    ClearBuffer();
                    throw new ArgumentException(channel + " is not a valid channel name.");
                }
            }
        }
        /// <summary>Request the topic for the given channel.</summary>
        /// <remarks>
        /// <para>
        /// The reply will be sent via the <see cref="Listener.OnRecieveTopic"/> event. If there is no topic
        /// then <see cref="Listener.OnError"/> will be called with a code of <see cref="ReplyCode.RPL_NOTOPIC"/>.
        /// </para>
        /// Possible Errors
        /// <list type="bullet">
        /// 		<item><description>ERR_NEEDMOREPARAMS</description></item>
        /// 		<item><description>ERR_NOTONCHANNEL</description></item>
        /// 		<item><description>ERR_CHANOPRIVSNEEDED</description></item>
        /// 		<item><description>ERR_NOCHANMODES</description></item>
        /// </list>
        /// </remarks>
        /// <param name="channel">The target channel.</param>
        /// <exception cref="ArgumentException">If the channel name is not valid.</exception> 
        /// <seealso cref="Listener.OnRecieveTopic"/> 
        public void RequestTopic(string channel)
        {
            lock (this)
            {
                if (Rfc2812Util.IsValidChannelName(channel))
                {
                    Buffer.Append("TOPIC");
                    Buffer.Append(SPACE);
                    Buffer.Append(channel);
                    Connection.SendCommand(Buffer);
                }
                else
                {
                    ClearBuffer();
                    throw new ArgumentException(channel + " is not a valid channel name.");
                }
            }
        }
        /// <summary>
        /// Leave the given channel.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// <list type="bullet">
        /// 		<item><description>ERR_NEEDMOREPARAMS</description></item>
        /// 		<item><description>ERR_NOSUCHCHANNEL</description></item>
        /// 		<item><description>ERR_NOTONCHANNEL</description></item>
        /// </list>
        /// </remarks>
        /// <param name="reason">A goodbye message.</param>
        /// <param name="channels">One or more channels to leave.</param>
        /// <example><code>
        /// //Leave a single channel
        /// connection.Sender.Part("Goodbye", "#test" );
        /// //Leave several at once
        /// connection.Sender.Part( "Goodbye", "#test","#alpha",#"bravo" );
        /// </code></example>
        /// <exception cref="ArgumentException">If the channel name is not valid or the reason is null.</exception> 
        /// <seealso cref="Listener.OnPart"/> 
        public void Part(string reason, params string[] channels)
        {
            lock (this)
            {
                if (IsEmpty(reason))
                {
                    ClearBuffer();
                    throw new ArgumentException("Part reason cannot be empty or null.");
                }
                if (Rfc2812Util.IsValidChannelList(channels))
                {
                    Buffer.Append("PART");
                    Buffer.Append(SPACE);
                    string channelList = String.Join(",", channels);
                    Buffer.Append(channelList);
                    Buffer.Append(SPACE_COLON);
                    // 9 is PART + 2 x Spaces + : + CR + LF
                    reason = Truncate(reason, 9);
                    Buffer.Append(reason);
                    Connection.SendCommand(Buffer);
                }
                else
                {
                    ClearBuffer();
                    throw new ArgumentException("One of the channels names is not valid.");
                }
            }
        }
        /// <summary>
        /// Leave a channel without giving a reason.
        /// </summary>
        /// <param name="channel">The channel to leave.</param>
        /// <exception cref="ArgumentException">If the channel name is not valid.</exception> 
        /// <seealso cref="Listener.OnPart"/> 
        public void Part(string channel)
        {
            lock (this)
            {
                if (Rfc2812Util.IsValidChannelName(channel))
                {
                    Buffer.Append("PART");
                    Buffer.Append(SPACE);
                    Buffer.Append(channel);
                    Connection.SendCommand(Buffer);
                }
                else
                {
                    ClearBuffer();
                    throw new ArgumentException(channel + " is not a valid channel name.");
                }
            }
        }

        public void Ping()
        {
            lock(this)
            {
                Buffer.Append("PING");
                Buffer.Append(SPACE);
                Buffer.Append("ping");
                Connection.SendCommand(Buffer);
            }
        }

        /// <summary>Send a notice to a channel.</summary>
        /// <remarks>
        /// <para>The difference between a notice and a normal message is that 
        /// automatic replies must never be sent in response to a notice. This rule 
        /// applies to servers too - they must not send any error reply back to the 
        /// client on receipt of a notice. The object of this rule is to avoid loops
        /// between clients automatically sending something in response to
        /// something it received. See <see cref="Sender.PublicMessage"/> for possible errors.</para>
        /// </remarks>
        /// <param name="channel">The target channel.</param>
        /// <param name="message">Text message. If the text is too large to be sent in one
        /// piece it will be broken up into smaller strings which will then
        /// be sent individually.</param>
        /// <exception cref="ArgumentException">If the channel name is not valid or the message is empty or null.</exception> 
        /// <seealso cref="Listener.OnPublicNotice"/> 
        public void PublicNotice(string channel, string message)
        {
            lock (this)
            {
                if (IsEmpty(message))
                {
                    ClearBuffer();
                    throw new ArgumentException("Notice message cannot be null or empty.");
                }
                if (Rfc2812Util.IsValidChannelName(channel))
                {
                    // 11 is NOTICE + 2 x Spaces + : + CR + LF
                    int max = MAX_COMMAND_SIZE - 11 - channel.Length;
                    if (message.Length > max)
                    {
                        string[] parts = BreakUpMessage(message, max);
                        foreach (string part in parts)
                        {
                            SendMessage("NOTICE", channel, part);
                        }
                    }
                    else
                    {
                        SendMessage("NOTICE", channel, message);
                    }
                }
                else
                {
                    ClearBuffer();
                    throw new ArgumentException(channel + " is not a valid channel name.");
                }
            }
        }
        /// <summary>Send a notice to a user.</summary>
        /// <remarks>
        /// <para>The difference between a notice and a normal message is that 
        /// automatic replies must never be sent in response to a notice. This rule 
        /// applies to servers too - they must not send any error reply back to the 
        /// client on receipt of a notice. The object of this rule is to avoid loops
        /// between clients automatically sending something in response to
        /// something it received. See <see cref="Sender.PrivateMessage"/> for possible errors.</para>
        /// </remarks>
        /// <param name="nick">The target nickname.</param>
        /// <param name="message">Text message. If the text is too large to be sent in one
        /// piece it will be broken up into smaller strings which will then
        /// be sent individually.</param>
        /// <exception cref="ArgumentException">If the nick is not valid or the message is empty or null.</exception> 
        /// <seealso cref="Listener.OnPrivateNotice"/> 
        public void PrivateNotice(string nick, string message)
        {
            lock (this)
            {
                if (IsEmpty(message))
                {
                    ClearBuffer();
                    throw new ArgumentException("Notice message cannot be empty or null.");
                }
                if (Rfc2812Util.IsValidNick(nick))
                {
                    // 11 is NOTICE + 2 x Spaces + : + CR + LF
                    int max = MAX_COMMAND_SIZE - 11 - nick.Length;
                    if (message.Length > max)
                    {
                        string[] parts = BreakUpMessage(message, max);
                        foreach (string part in parts)
                        {
                            SendMessage("NOTICE", nick, part);
                        }
                    }
                    else
                    {
                        SendMessage("NOTICE", nick, message);
                    }
                }
                else
                {
                    ClearBuffer();
                    throw new ArgumentException(nick + " is not a valid nickname.");
                }
            }
        }
        /// <summary>
        /// Send a message to all the users in a channel.</summary>
        /// <remarks>
        /// Possible Errors
        /// <list type="bullet">
        /// 		<item><description>ERR_CANNOTSENDTOCHAN</description></item>
        /// 		<item><description>ERR_NOTEXTTOSEND</description></item>
        /// </list>
        /// </remarks>
        /// <param name="channel">The target channel.</param>
        /// <param name="message">A message. If the message is too long it will be broken
        /// up into smaller piecese which will be sent sequentially.</param>
        /// <exception cref="ArgumentException">If the channel name is not valid or if the message is null.</exception> 
        /// <seealso cref="Listener.OnPublic"/> 
        public void PublicMessage(string channel, string message)
        {
            lock (this)
            {
                if (IsEmpty(message))
                {
                    ClearBuffer();
                    throw new ArgumentException("Public message cannot be null or empty.");
                }
                if (Rfc2812Util.IsValidChannelName(channel))
                {
                    // 11 is PRIVMSG + 2 x Spaces + : + CR + LF
                    int max = MAX_COMMAND_SIZE - 11 - channel.Length;
                    if (message.Length > max)
                    {
                        string[] parts = BreakUpMessage(message, max);
                        foreach (string part in parts)
                        {
                            SendMessage("PRIVMSG", channel, part);
                        }
                    }
                    else
                    {
                        SendMessage("PRIVMSG", channel, message);
                    }
                }
                else
                {
                    ClearBuffer();
                    throw new ArgumentException(channel + " is not a valid channel name.");
                }
            }
        }
        /// <summary>
        /// Send a message to a user.</summary>
        /// <remarks>
        /// <para>If the target user status is away, the <see cref="Listener.OnAway"/> event will be
        /// called along with the away message if any.
        /// </para>
        /// Possible Errors
        /// <list type="bullet">
        /// 		<item><description>ERR_NORECIPIENT</description></item>
        /// 		<item><description>ERR_NOTEXTTOSEND</description></item>
        /// 		<item><description>ERR_NOSUCHNICK</description></item>
        /// </list>
        /// </remarks>
        /// <param name="nick">The target user.</param>
        /// <param name="message">A message. If the message is too long it will be broken
        /// up into smaller piecese which will be sent sequentially.</param>
        /// <exception cref="ArgumentException">If the nickname is not valid or if the message is null or empty.</exception> 
        /// <seealso cref="Listener.OnPrivate"/> 
        public void PrivateMessage(string nick, string message)
        {
            lock (this)
            {
                if (IsEmpty(message))
                {
                    ClearBuffer();
                    throw new ArgumentException("Private message cannot be null or empty.");
                }
                if (Rfc2812Util.IsValidNick(nick))
                {
                    // 11 is PRIVMSG + 2 x Spaces + : + CR + LF
                    int max = MAX_COMMAND_SIZE - 11 - nick.Length;
                    if (message.Length > max)
                    {
                        string[] parts = BreakUpMessage(message, max);
                        foreach (string part in parts)
                        {
                            SendMessage("PRIVMSG", nick, part);
                        }
                    }
                    else
                    {
                        SendMessage("PRIVMSG", nick, message);
                    }
                }
                else
                {
                    ClearBuffer();
                    throw new ArgumentException(nick + " is not a valid nickname.");
                }
            }
        }
        /// <summary>
        /// Invite a user to a channel.</summary>
        /// <remarks>
        /// <para>The parameter nickname is the nickname of the person to be invited to
        /// the target channel channel. There is no requirement that the
        /// channel the target user is being invited to must exist or be a valid
        /// channel. However, if the channel exists, only members of the channel
        /// are allowed to invite other users. When the channel has invite-only
        /// flag set, only channel operators may an invite.</para>
        /// 
        /// <para>Only the user inviting and the user being invited will receive
        /// notification of the invitation. Other channel members are not
        /// notified. (This is unlike the mode changes, and is occasionally the
        /// source of trouble for users.)</para>
        /// 
        /// <para>After the invite is sent the IRC server will signal that it 
        /// was correctly received by calling <see cref="Listener.OnInviteSent"/>.
        /// </para>
        /// 
        /// Possible Errors
        /// <list type="bullet">
        /// 		<item><description>ERR_NEEDMOREPARAMS</description></item>
        /// 		<item><description>ERR_NOSUCHNICK</description></item>
        /// 		<item><description>ERR_NOTONCHANNEL</description></item>
        /// 		<item><description>ERR_USERONCHANNEL</description></item>
        /// 		<item><description>ERR_CHANOPRIVSNEEDED</description></item>
        /// </list>
        /// </remarks>
        /// <param name="who">The nick of the person to invite</param>
        /// <param name="channel">The channel they are invited to join.</param>
        /// <exception cref="ArgumentException">If the nickname or channel is not valid.</exception> 
        /// <seealso cref="Listener.OnInviteSent"/> 
        /// <seealso cref="Listener.OnInvite"/> 
        public void Invite(string who, string channel)
        {
            lock (this)
            {
                if (!Rfc2812Util.IsValidNick(who))
                {
                    ClearBuffer();
                    throw new ArgumentException(who + " is not a valid nickname.");
                }
                if (!Rfc2812Util.IsValidChannelName(channel))
                {
                    ClearBuffer();
                    throw new ArgumentException(channel + " is not a valid channel.");
                }
                Buffer.Append("INVITE");
                Buffer.Append(SPACE);
                Buffer.Append(who);
                Buffer.Append(SPACE);
                Buffer.Append(channel);
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>Kick a user off a channel.</summary>
        /// <remarks>
        /// Possible Replies
        /// <list type="bullet">
        /// 		<item><description>ERR_NEEDMOREPARAMS</description></item>
        /// 		<item><description>ERR_NOSUCHCHANNEL</description></item>
        /// 		<item><description>ERR_BADCHANMASK</description></item>
        /// 		<item><description>ERR_CHANOPRIVSNEEDED</description></item>
        /// 		<item><description>ERR_USERNOTINCHANNEL</description></item>
        /// 		<item><description>ERR_NOTONCHANNEL</description></item>
        /// 	</list>
        /// </remarks>
        /// <param name="nicks">One or more users to kick.</param>
        /// <param name="channel">Which channel to kick the user from.</param>
        /// <param name="reason">Why.</param>
        /// <example><code>
        /// //Kick a single user
        /// connection.Sender.Kick("#thresher","No Perl please","lwall" );
        /// //Kicks several users at once
        /// connection.Sender.Kick( "#thresher", "Bye", "John","Dick","Harry" );
        /// </code></example>
        /// <exception cref="ArgumentException">If the nickname or channel is not valid or the reason is null.</exception> 
        /// <seealso cref="Listener.OnKick"/> 
        public void Kick(string channel, string reason, params string[] nicks)
        {
            lock (this)
            {
                if (!Rfc2812Util.IsValidNicklList(nicks))
                {
                    ClearBuffer();
                    throw new ArgumentException("One of the nicknames is invalid.");
                }
                if (!Rfc2812Util.IsValidChannelName(channel))
                {
                    ClearBuffer();
                    throw new ArgumentException(channel + " is not a valid channel.");
                }
                if (IsEmpty(reason))
                {
                    ClearBuffer();
                    throw new ArgumentException("The reason for kicking cannot be null.");
                }
                string nickList = String.Join(",", nicks);
                // 10 is KICK + 3 x Spaces + : + CR + LF
                reason = Truncate(reason, 10 + channel.Length + nickList.Length);
                Buffer.Append("KICK");
                Buffer.Append(SPACE);
                Buffer.Append(channel);
                Buffer.Append(SPACE);
                Buffer.Append(nickList);
                Buffer.Append(SPACE_COLON);
                Buffer.Append(reason);
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Request to know if a user is currenlty on IRC.</summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NEEDMOREPARAMS</description></item>
        /// 		</list>
        /// </remarks>
        /// <param name="nick">Target user.</param>
        /// <example><code>
        /// //Query for a nick
        /// connection.Sender.Ison( "joe" );
        /// </code></example>
        /// <exception cref="ArgumentException">If the nickname is not valid.</exception> 
        /// <seealso cref="Listener.OnIson"/> 
        public void Ison(string nick)
        {
            lock (this)
            {
                if (!Rfc2812Util.IsValidNick(nick))
                {
                    ClearBuffer();
                    throw new ArgumentException(nick + " is not a valid nick.");
                }
                Buffer.Append("ISON");
                Buffer.Append(SPACE);
                Buffer.Append(nick);
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Request a list of users who match a given query.</summary>
        /// <remarks>
        /// Possible Errors
        /// <list type="bullet">
        /// 		<item><description>ERR_TOOMANYLINES</description></item>
        /// </list>
        /// </remarks>
        /// <param name="mask">The mask passed to Who is matched against a users' host, 
        /// real name or nickname. It uses the wildcard system of matching
        /// where the '*' can stand for any number of characters and '?' stands for any single
        /// character. The query will only match against one component so it is not possible
        /// to match against both nick and host for example.
        /// </param>
        /// <param name="operatorsOnly"> True if the query should only try to match the mask
        /// to users designated operators.</param>
        /// <example><code>
        /// //Find all users from Russia, i.e. who have .ru in their hostnames
        /// connection.Sender.Who("*.ru", false );
        /// //Find all users from clan [DX], i.e. have '[DX]' in their nick
        /// connection.Sender.Who("[DX]*", false );
        /// </code></example>
        /// <exception cref="ArgumentException">If the mask is null,empty, or too long.</exception> 
        /// <seealso cref="Listener.OnWho"/> 
        public void Who(string mask, bool operatorsOnly)
        {
            lock (this)
            {
                //7 is WHO + Space +O + CR + LF
                int max = MAX_COMMAND_SIZE - 7;
                if (IsEmpty(mask) ||
                    mask.Length > max)
                {
                    ClearBuffer();
                    throw new ArgumentException("Who mask is invalid.");
                }
                Buffer.Append("WHO");
                Buffer.Append(SPACE);
                Buffer.Append(mask);
                if (operatorsOnly)
                {
                    Buffer.Append(SPACE);
                    Buffer.Append("o");
                }
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Request a list all visible users (whose mode is not +i) and those 
        /// who don't have a common channel with the requesting 
        /// client are listed. 
        /// </summary>
        /// <seealso cref="Listener.OnWho"/> 
        public void AllWho()
        {
            lock (this)
            {
                Buffer.Append("WHO");
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Request detailed information about a given user.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NOSUCHNICK</description></item>
        /// 		</list>
        /// </remarks>
        /// <param name="nick">The nick of the query subject.</param>
        /// <exception cref="ArgumentException">If the nick is invalid.</exception> 
        /// <seealso cref="Listener.OnWhois"/> 
        /// <seealso cref="WhoisInfo"/> 
        public void Whois(string nick)
        {
            lock (this)
            {
                if (!Rfc2812Util.IsValidNick(nick))
                {
                    ClearBuffer();
                    throw new ArgumentException(nick + " is not a valid nickname.");
                }
                Buffer.Append("WHOIS");
                Buffer.Append(SPACE);
                Buffer.Append(nick);
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Set the user status to away and set an automatic reply 
        /// to any private message.
        /// </summary>
        /// <remarks>
        /// This functions as a kind of IRC answering machine. It is normally used
        /// to indicate that the user is away from IRC.
        /// </remarks>
        /// <param name="message">The message that will be sent back to others when you
        /// are away. Overly long message will be truncated.</param>
        /// <exception cref="ArgumentException">If the message is null or empty.</exception> 
        /// <seealso cref="Listener.OnAway"/> 
        public void Away(string message)
        {
            lock (this)
            {
                if (IsEmpty(message))
                {
                    ClearBuffer();
                    throw new ArgumentException("Away message cannot be empty or null.");
                }
                Buffer.Append("AWAY");
                Buffer.Append(SPACE_COLON);
                // 8 is AWAY + Space + : + CR + LF
                message = Truncate(message, 8);
                Buffer.Append(message);
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Turns off the away status and the accompanying message.
        /// </summary>
        public void UnAway()
        {
            lock (this)
            {
                Buffer.Append("AWAY");
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Request information about a user who is no longer on IRC.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NONICKNAMEGIVEN</description></item>
        /// 			<item><description>ERR_WASNOSUCHNICK</description></item>
        /// 		</list>
        /// </remarks>
        /// <param name="nick">Target nick</param>
        /// <exception cref="ArgumentException">If the nick is invalid.</exception> 
        /// <seealso cref="Listener.OnWhowas"/> 
        public void Whowas(string nick)
        {
            lock (this)
            {
                if (!Rfc2812Util.IsValidNick(nick))
                {
                    ClearBuffer();
                    throw new ArgumentException(nick + " is not a valid nickname.");
                }
                Buffer.Append("WHOWAS");
                Buffer.Append(SPACE);
                Buffer.Append(nick);
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Request information about a user who is no longer on IRC
        /// but with a maximum number of responses.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NONICKNAMEGIVEN</description></item>
        /// 			<item><description>ERR_WASNOSUCHNICK</description></item>
        /// 		</list>
        /// </remarks>
        /// <param name="count">The maximum number of replies the IRC server
        /// should send back.</param>
        /// <param name="nick">Target nick</param>
        /// <exception cref="ArgumentException">If the nick is invalid or if count is less 
        /// than or equal to zero.</exception> 
        /// <seealso cref="Listener.OnWhowas"/> 
        public void Whowas(string nick, int count)
        {
            lock (this)
            {
                if (!Rfc2812Util.IsValidNick(nick))
                {
                    ClearBuffer();
                    throw new ArgumentException(nick + " is not a valid nickname.");
                }
                if (count < 1)
                {
                    ClearBuffer();
                    throw new ArgumentException("Count must be more than zero.");
                }
                Buffer.Append("WHOWAS");
                Buffer.Append(SPACE);
                Buffer.Append(nick);
                Buffer.Append(SPACE);
                Buffer.Append(count);
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Request the modes set for this user.
        /// </summary>
        /// <seealso cref="Listener.OnUserModeRequest"/>
        /// <seealso cref="UserMode"/>
        public void RequestUserModes()
        {
            lock (this)
            {
                Buffer.Append("MODE");
                Buffer.Append(SPACE);
                Buffer.Append(Connection.ConnectionData.Nick);
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>Changes this client's mode. To change another nick's mode
        /// use <see cref="ChangeChannelMode"/>.</summary>
        /// <remarks>
        /// Away cannot be set here but should be set using <see cref="Sender.Away"/> 
        /// or removed using <see cref="Sender.UnAway"/>.
        /// </remarks>
        /// <param name="action">Add or remove a mode.</param>
        /// <param name="mode">The mode to be changed.</param>
        /// <example><code>
        /// //Turn off invisibility
        /// connection.Sender.ChangeUserMode( ModeAction.Remove, UserMode.Invisible );
        /// //Turn on wallops (and get a lot of IRC garbage)
        /// connection.Sender.ChangeUserMode( ModeAction.Add, UserMode.Wallops );
        /// </code></example>
        /// <exception cref="ArgumentException">If the UserMode parameter is Away.</exception> 
        /// <seealso cref="Listener.OnUserModeChange"/>
        public void ChangeUserMode(ModeAction action, UserMode mode)
        {
            lock (this)
            {
                if (mode == UserMode.Away)
                {
                    ClearBuffer();
                    throw new ArgumentException("Away mode can only be changed with the Away and Unaway commands.");
                }
                Buffer.Append("MODE");
                Buffer.Append(SPACE);
                Buffer.Append(Connection.ConnectionData.Nick);
                Buffer.Append(SPACE);
                Buffer.Append(Rfc2812Util.ModeActionToChar(action));
                Buffer.Append(Rfc2812Util.UserModeToChar(mode));
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Change a channel's mode.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NEEDMOREPARAMS</description></item>
        /// 			<item><description>ERR_KEYSET</description></item>
        /// 			<item><description>ERR_NOCHANMODES</description></item>
        /// 			<item><description>ERR_CHANOPRIVSNEEDED</description></item>
        /// 			<item><description>ERR_USERNOTINCHANNEL</description></item>
        /// 			<item><description>ERR_UNKNOWNMODE</description></item>
        /// 		</list>
        /// </remarks>
        /// <param name="channel">The target channel.</param>
        /// <param name="action">Add or remove.</param>
        /// <param name="mode">The target mode.</param>
        /// <param name="param">An optional parameter for certain modes. If the mode 
        /// does not require one this should be null.</param>
        /// <example><code>
        /// //Give 'nick' the ability to talk on a moderated channel, i.e. add Voice
        /// connection.Sender.ChangeChannelMode("#thresher", ModeAction.Add, ChannelMode.Voice,"nick" );
        /// //Make a channel private
        /// connection.Sender.ChangeChannelMode( "#thresher", ModeAction.Add, ChannelMode.Private, null );
        /// </code></example>
        /// <exception cref="ArgumentException">If the channel name is invalid.</exception> 
        /// <seealso cref="Listener.OnChannelModeChange"/>
        public void ChangeChannelMode(string channel, ModeAction action, ChannelMode mode, string param)
        {
            lock (this)
            {
                if (!Rfc2812Util.IsValidChannelName(channel))
                {
                    ClearBuffer();
                    throw new ArgumentException(channel + " is not a valid channel.");
                }
                Buffer.Append("MODE");
                Buffer.Append(SPACE);
                Buffer.Append(channel);
                Buffer.Append(SPACE);
                Buffer.Append(Rfc2812Util.ModeActionToChar(action));
                Buffer.Append(Rfc2812Util.ChannelModeToChar(mode));
                if (!IsEmpty(param))
                {
                    Buffer.Append(SPACE);
                    Buffer.Append(param);
                }
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Request the list of users that a channel keeps for the given mode.. 
        /// </summary>
        /// <remarks>
        /// Each channel maintains a list of those banned, those excepted from a ban,
        /// those on automatic invite, and the channel creator. Use this method to retreieve one
        /// of those lists.
        /// </remarks>
        /// <param name="channel">The taregt channel.</param>
        /// <param name="mode">Must be one of:
        /// Ban, Exception, Invitation, or ChannelCreator.
        /// </param>
        /// <example><code>
        /// //Request the channel's banned list
        /// connection.Sender.RequestChannelList("#thresher", ChannelMode.Ban );
        /// </code></example>
        /// <exception cref="ArgumentException">If the channel is invalid or the ChannelMode is
        /// not one of the 4 allowed types.</exception> 
        /// <seealso cref="Listener.OnChannelList"/>
        public void RequestChannelList(string channel, ChannelMode mode)
        {
            lock (this)
            {
                if (!Rfc2812Util.IsValidChannelName(channel))
                {
                    ClearBuffer();
                    throw new ArgumentException(channel + " is not a valid channel.");
                }
                if (mode != ChannelMode.Ban &&
                    mode != ChannelMode.Exception &&
                    mode != ChannelMode.Invitation &&
                    mode != ChannelMode.ChannelCreator)
                {
                    ClearBuffer();
                    throw new ArgumentException(Enum.GetName(typeof(ChannelMode), mode) + " is not a valid channel mode for this request.");
                }
                Buffer.Append("MODE");
                Buffer.Append(SPACE);
                Buffer.Append(channel);
                Buffer.Append(SPACE);
                Buffer.Append(Rfc2812Util.ChannelModeToChar(mode));
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Request the modes of a channel.
        /// </summary>
        /// <param name="channel">The target channel.</param>
        /// <exception cref="ArgumentException">If the channel is invalid, null, or empty.</exception> 
        /// <seealso cref="Listener.OnChannelModeRequest"/>
        public void RequestChannelModes(string channel)
        {
            lock (this)
            {
                if (!Rfc2812Util.IsValidChannelName(channel))
                {
                    ClearBuffer();
                    throw new ArgumentException(channel + " is not a valid channel.");
                }
                Buffer.Append("MODE");
                Buffer.Append(SPACE);
                Buffer.Append(channel);
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Send an action message to a channel.
        /// </summary>
        /// <remarks>
        /// This is actually a CTCP command but it is so widely used
        /// that it is included here. These are the '\me Laughs' type messages. 
        /// </remarks>
        /// <param name="channel">The target channel.</param>
        /// <param name="description">A description of the action. If this is too long it will
        /// be truncated.</param>
        /// <example><code>
        /// //Express an emotion...
        /// connection.Sender.Action("#thresher", "Kicks down the door" );
        /// </code></example>
        /// <exception cref="ArgumentException">If the channel name is not valid. Will
        /// also be thrown if the description is null or empty.</exception> 
        /// <seealso cref="Listener.OnAction"/>
        public void Action(string channel, string description)
        {
            lock (this)
            {
                if (IsEmpty(description))
                {
                    ClearBuffer();
                    throw new ArgumentException("Action description cannot be null or empty.");
                }
                if (Rfc2812Util.IsValidChannelName(channel))
                {
                    // 19 is PRIVMSG + 2 x Spaces + : + CR + LF + 2xCtcpQuote + ACTION
                    description = Truncate(description, 19 + channel.Length);
                    SendMessage("PRIVMSG", channel, CtcpQuote + "ACTION " + description + CtcpQuote);
                }
                else
                {
                    ClearBuffer();
                    throw new ArgumentException(channel + " is not a valid channel name.");
                }
            }
        }
        /// <summary>
        /// Send an action message to a user instead of a channel.
        /// </summary>
        /// <param name="nick">The target user.</param>
        /// <param name="description">A description of the action. If this is too long it will
        /// be truncated.</param>
        /// <exception cref="ArgumentException">If the nickname is not valid. Will
        /// also be thrown if the description is null or empty.</exception>
        /// <seealso cref="Listener.OnPrivateAction"/>
        public void PrivateAction(string nick, string description)
        {
            lock (this)
            {
                if (IsEmpty(description))
                {
                    ClearBuffer();
                    throw new ArgumentException("Action description cannot be null or empty.");
                }
                if (Rfc2812Util.IsValidNick(nick))
                {
                    // 19 is PRIVMSG + 2 x Spaces + : + CR + LF + 2xCtcpQuote + ACTION
                    description = Truncate(description, 19 + nick.Length);
                    SendMessage("PRIVMSG", nick, CtcpQuote + "ACTION " + description + CtcpQuote);
                }
                else
                {
                    ClearBuffer();
                    throw new ArgumentException(nick + " is not a valid nickname.");
                }
            }
        }
        /// <summary>Register this connection with the IRC server.</summary>
        /// <remarks>
        /// This method should be called when the initial attempt
        /// to register with the IRC server fails because the nick is already
        /// taken. To be informed when this fails you must be subscribed
        /// to <see cref="Listener.OnNickError"/>. If <see cref="Connection.HandleNickTaken"/>
        /// is set to true (which is its default value) then Thresher will automatically
        /// create an alternate nick and use that. The new nick can be retrieved
        /// by calling <see cref="Connection.ConnectionData.Nick"/>.
        /// </remarks>
        /// <param name="newNick">The changed nick name.</param>
        /// <seealso cref="NameGenerator"/>
        public void Register(string newNick)
        {
            Connection.connectionArgs.Nick = newNick;
            Nick(Connection.connectionArgs.Nick);
            User(Connection.connectionArgs);
        }
        /// <summary>
        /// Send an arbitrary text message to the IRC server.
        /// </summary>
        /// <remarks>
        /// Messages that are too long will be truncated. There is no corresponding 
        /// events so it will be necessary to check for standard reply codes and possibly
        /// errors.
        /// </remarks>
        /// <param name="message">A text message.</param>
        /// <exception cref="ArgumentException">If the message is null or empty.</exception> 
        public void Raw(string message)
        {
            lock (this)
            {
                if (IsEmpty(message))
                {
                    ClearBuffer();
                    throw new ArgumentException("Message cannot be null or empty.");
                }
                if (message.Length > MAX_COMMAND_SIZE)
                {
                    message = message.Substring(0, MAX_COMMAND_SIZE);
                }
                Buffer.Append(message);
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Request the version of the IRC server program.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NOSUCHSERVER</description></item>
        /// 		</list>
        /// </remarks>
        /// <seealso cref="Listener.OnVersion"/>
        public void Version()
        {
            Version(null);
        }
        /// <summary>
        /// Used to query the version of the IRC server program.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NOSUCHSERVER</description></item>
        /// 		</list>
        /// </remarks>
        /// <param name="targetServer">The FQDN of the IRC server to query. Wildcards are allowed.
        /// Must be a server part of the same IRC network this connection is connected to.</param>
        /// <seealso cref="Listener.OnVersion"/>
        public void Version(string targetServer)
        {
            lock (this)
            {
                Buffer.Append("VERSION");
                if (!IsEmpty(targetServer))
                {
                    //10 is VERSION + 1 x Spaces + CR + LF 
                    targetServer = Truncate(targetServer, 10);
                    Buffer.Append(SPACE);
                    Buffer.Append(targetServer);
                }
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Request the "Message Of The Day" from the current server.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NOMOTD</description></item>
        /// 		</list>
        /// </remarks>
        /// <seealso cref="Listener.OnMotd"/>
        public void Motd()
        {
            Motd(null);
        }
        /// <summary>
        /// Request the "Message Of The Day" from the given server.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NOMOTD</description></item>
        /// 		</list>
        /// </remarks>
        /// <param name="targetServer">The FQDN of the IRC server to query. Wildcards are allowed.
        /// Must be a server part of the same IRC network this connection is connected to.</param>
        /// <seealso cref="Listener.OnMotd"/>
        public void Motd(string targetServer)
        {
            lock (this)
            {
                Buffer.Append("MOTD");
                if (!IsEmpty(targetServer))
                {
                    //7 is MOTD + 1 x Spaces + CR + LF 
                    targetServer = Truncate(targetServer, 7);
                    Buffer.Append(SPACE);
                    Buffer.Append(targetServer);
                }
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Request the local time from the current server.
        /// </summary>
        /// <seealso cref="Listener.OnTime"/>
        public void Time()
        {
            Time(null);
        }
        /// <summary>
        /// Request the local time from the given server.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NOSUCHSERVER</description></item>
        /// 		</list>
        /// </remarks>
        /// <param name="targetServer">The FQDN of the IRC server to query. Wildcards are allowed.
        /// Must be a server part of the same IRC network this connection is connected to.</param>
        /// <seealso cref="Listener.OnTime"/>
        public void Time(string targetServer)
        {
            lock (this)
            {
                Buffer.Append("TIME");
                if (!IsEmpty(targetServer))
                {
                    //8 is TIME + 1 x Spaces + CR + LF 
                    targetServer = Truncate(targetServer, 8);
                    Buffer.Append(SPACE);
                    Buffer.Append(targetServer);
                }
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Send a message to all users who have the 'w' user mode set.</summary>
        /// <remarks>
        /// This will likely be forbidden to all but IRC
        /// OPS.
        /// </remarks>
        /// <param name="message">Any text message.</param>
        /// <exception cref="ArgumentException">If the message is empty or null.</exception>
        public void Wallops(string message)
        {
            lock (this)
            {
                if (IsEmpty(message))
                {
                    ClearBuffer();
                    throw new ArgumentException("Wallops message cannot be null or empty.");
                }
                Buffer.Append("WALLOPS");
                // 11 is WALLOPS + 1 x Spaces + CR + LF
                message = Truncate(message, 10);
                Buffer.Append(SPACE);
                Buffer.Append(message);
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Request information about the software
        /// of the current IRC server.
        /// </summary>
        /// <remarks>
        /// This returns information describing the
        /// server: its version, when it was compiled, the patchlevel, when it
        /// was started, and any other miscellaneous information which may be
        /// 	considered relevant.
        /// </remarks>
        /// <seealso cref="Listener.OnInfo"/>
        public void Info()
        {
            Info(null);
        }
        /// <summary>
        /// Request information about the software
        /// of the target IRC server.
        /// </summary>
        /// <remarks>
        /// <para>This returns information describing the
        /// server: its version, when it was compiled, the patchlevel, when it
        /// was started, and any other miscellaneous information which may be
        /// 	considered relevant.</para>
        /// 	
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NOSUCHSERVER</description></item>
        /// 		</list>
        /// </remarks>
        /// <param name="target">Either a user nickname or a specific IRC server connected
        /// to the current network. If it is a nickname then return the information about
        /// the server to which 'nick' is connected. Can include wildcards.</param>
        /// <example><code>
        /// //Query a specific server
        /// connection.Sender.Info( "sunray.sharkbite.org" );
        /// //Query the server Bob is connected to
        /// connection.Sender.Info("Bob");
        /// </code></example>
        /// <seealso cref="Listener.OnInfo"/>
        public void Info(string target)
        {
            lock (this)
            {
                Buffer.Append("INFO");
                if (!IsEmpty(target))
                {
                    //7 is INFO + 1 x Spaces + CR + LF 
                    target = Truncate(target, 7);
                    Buffer.Append(SPACE);
                    Buffer.Append(target);
                }
                Connection.SendCommand(Buffer);
            }
        }
        /// <summary>
        /// Request information about the administrator
        /// of the current IRC server.
        /// </summary>
        /// <remarks>
        /// This returns information such as the administrator's
        /// email address, geographical location and whatever else
        /// the IRC is configured to send as a response.
        /// </remarks>
        /// <seealso cref="Listener.OnAdmin"/>
        public void Admin()
        {
            Admin(null);
        }
        /// <summary>
        /// Request information about the administrator
        /// of the target IRC server.
        /// </summary>
        /// <remarks>
        /// <para> This returns information such as the administrator's
        /// email address, geographical location and whatever else
        /// the IRC is configured to send as a response.
        /// </para>
        /// 	
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NOSUCHSERVER</description></item>
        /// 		</list>
        /// </remarks>
        /// <param name="target">Either a user nickname or a specific IRC server connected
        /// to the current network. If it is a nickname then return the information about
        /// the server to which 'nick' is connected. Can include wildcards.</param>
        /// <example><code>
        /// //Request info about the administrator of the specified server
        /// connection.Sender.Admin( "sunray.sharkbite.org" );
        /// //Request info about the administrator of the server Bob is connected to
        /// connection.Sender.Admin("Bob");
        /// </code></example>
        /// <seealso cref="Listener.OnAdmin"/>
        public void Admin(string target)
        {
            lock (this)
            {
                Buffer.Append("ADMIN");
                if (!IsEmpty(target))
                {
                    //8 is INFO + 1 x Spaces + CR + LF 
                    target = Truncate(target, 8);
                    Buffer.Append(SPACE);
                    Buffer.Append(target);
                }
                Connection.SendCommand(Buffer);
            }
        }

        /// <summary>
        /// Request statistics about the size of the IRC network.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NOSUCHSERVER</description></item>
        /// 		</list>
        /// </remarks>
        /// <seealso cref="Listener.OnLusers"/>
        public void Lusers()
        {
            Lusers(null, null);
        }

        /// <summary>
        /// Request statistics about the size of the IRC network.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NOSUCHSERVER</description></item>
        /// 		</list>
        /// </remarks>
        /// <param name="hostMask">Limits the kinds of servers included in the response by
        /// specifiying a hostname string. Can include wildcards.</param>
        /// <param name="targetServer">Specifies the server that should process the request. Can be null
        /// to indicate that the current server should handle the request. Can include wildcards.</param>
        /// <example><code>
        /// //Request stats from the current server
        /// connection.Sender.Lusers();
        /// //Request stats about all servers ending in '.net' from the current server
        /// connection.Sender.Lusers("*.net", null );
        /// //Request stats about all servers ending in '.net' from 'west.gamesnet.net'
        /// connection.Sender.Lusers("*.net", "west.gamesnet.net");
        /// </code></example>
        /// <exception cref="ArgumentException">If the host mask and server names are too long.</exception>
        /// <seealso cref="Listener.OnLusers"/>
        public void Lusers(string hostMask, string targetServer)
        {
            lock (this)
            {
                Buffer.Append("LUSERS");
                if (!IsEmpty(hostMask))
                {
                    Buffer.Append(SPACE);
                    Buffer.Append(hostMask);
                }
                if (!IsEmpty(targetServer))
                {
                    Buffer.Append(SPACE);
                    Buffer.Append(targetServer);
                }

                if (TooLong(Buffer))
                {
                    ClearBuffer();
                    throw new ArgumentException("Hostmask and TargetServer are too long.");
                }

                Connection.SendCommand(Buffer);
            }
        }

        /// <summary>
        /// Request all server names which are known by the current server.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NOSUCHSERVER</description></item>
        /// 		</list>
        /// </remarks>
        /// <seealso cref="Listener.OnLinks"/>
        public void Links()
        {
            Links(null);
        }

        /// <summary>
        /// Request all server names which are known by the target server
        /// and which match a given host mask.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NOSUCHSERVER</description></item>
        /// 		</list>
        /// </remarks>
        /// <param name="masks">Either a single string which acts as a host
        /// mask filter for the query. Or two strings with the first as host mask
        /// and the second a target server. Any other arguments will be ignored.</param>
        /// <example><code>
        /// //Request names from the current server
        /// connection.Sender.Links();
        /// //Request names of all servers ending in '.net' from the current server
        /// connection.Sender.Links("*.edu" );
        /// //Request names of all servers ending in '.edu' from '*.gnome.org' servers
        /// connection.Sender.Links("*.edu", "*.gnome.org");
        /// </code></example>
        /// <exception cref="ArgumentException">If the masks are too long.</exception>
        /// <seealso cref="Listener.OnLinks"/>
        public void Links(params string[] masks)
        {
            lock (this)
            {
                Buffer.Append("LINKS");
                if (masks != null)
                {
                    Buffer.Append(SPACE);
                    Buffer.Append(masks[0]);

                    if (masks.Length >= 2)
                    {
                        Buffer.Append(SPACE);
                        Buffer.Append(masks[1]);
                    }
                }

                if (TooLong(Buffer))
                {
                    ClearBuffer();
                    throw new ArgumentException("Masks are too long.");
                }
                Connection.SendCommand(Buffer);
            }
        }


        /// <summary>
        /// Request certain kinds of statistics about the current server.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NOSUCHSERVER</description></item>
        /// 		</list>
        /// </remarks>
        /// <param name="query">The type of query to send. See <see cref="StatsQuery"/> for choice.</param>
        /// <example><code>
        /// //Request server link stats
        /// connection.Sender.Stats( StatsQuery.Connections );
        /// </code></example>
        /// <seealso cref="Listener.OnStats"/>
        public void Stats(StatsQuery query)
        {
            Stats(query, null);
        }

        /// <summary>
        /// Request certain kinds of statistics about the current server.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NOSUCHSERVER</description></item>
        /// 		</list>
        /// </remarks>
        /// <param name="query">The type of query to send. See <see cref="StatsQuery"/> for choice.</param>
        /// <param name="targetServer">Specifies the server that should process the request. Can include wildcards.</param>
        /// <example><code>
        /// //Request list of Operators from the server 'irc.gnome.org'
        /// connection.Sender.Stats( StatsQuery.Operators, "irc.gnome.org" );
        /// </code></example>
        /// <exception cref="ArgumentException">If the target server name is too long.</exception>
        /// <seealso cref="Listener.OnStats"/>
        public void Stats(StatsQuery query, string targetServer)
        {
            lock (this)
            {
                Buffer.Append("STATS");
                Buffer.Append(SPACE);
                Buffer.Append(Rfc2812Util.StatsQueryToChar(query));
                if (targetServer != null)
                {
                    Buffer.Append(SPACE);
                    Buffer.Append(targetServer);
                }
                if (TooLong(Buffer))
                {
                    ClearBuffer();
                    throw new ArgumentException("Target server name is too long.");
                }
                Connection.SendCommand(Buffer);
            }
        }

        /// <summary>
        /// Forcefully disconnect a user form the IRC server. This can only be used
        /// by Operators.
        /// </summary>
        /// <remarks>
        /// Possible Errors
        /// 		<list type="bullet">
        /// 			<item><description>ERR_NOPRIVILEGES</description></item>
        /// 			<item><description>ERR_NEEDMOREPARAMS</description></item>
        /// 			<item><description>ERR_NOSUCHNICK</description></item>
        /// 			<item><description>ERR_CANTKILLSERVER</description></item>
        /// 		</list>
        /// </remarks>
        /// <param name="nick">User to kill</param>
        /// <param name="reason">The reason for disconnecting the user.</param>
        /// <exception cref="ArgumentException">If the nick is not valid or the reason is null.</exception> 
        /// <seealso cref="Listener.OnKill"/>
        public void Kill(string nick, string reason)
        {
            lock (this)
            {
                if (IsEmpty(nick))
                {
                    ClearBuffer();
                    throw new ArgumentException("Nick cannot be empty or null.");
                }
                if (IsEmpty(reason))
                {
                    ClearBuffer();
                    throw new ArgumentException("Reason cannot be empty or null.");
                }
                if (Rfc2812Util.IsValidNick(nick))
                {
                    Buffer.Append("KILL");
                    Buffer.Append(SPACE);
                    Buffer.Append(nick);
                    Buffer.Append(SPACE);
                    Buffer.Append(reason);
                    Connection.SendCommand(Buffer);
                }
                else
                {
                    ClearBuffer();
                    throw new ArgumentException(nick + " is not a valid nick name.");
                }
            }
        }

    }
}
